/*
 * File    : PEPieceImpl.java
 * Created : 15-Oct-2003
 * By      : Olivier
 * 
 * Azureus - a Java Bittorrent client
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details ( see the LICENSE file ).
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package org.gudy.azureus2.core3.peer.impl;

/**
 * @author parg
 * @author MjrTom
 *			2005/Oct/08: numerous changes for new piece-picking
 *			2006/Jan/02: refactoring piece picking to elsewhere, and consolidations
 */

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.gudy.azureus2.core3.disk.DiskManager;
import org.gudy.azureus2.core3.disk.DiskManagerPiece;
import org.gudy.azureus2.core3.logging.LogEvent;
import org.gudy.azureus2.core3.logging.LogIDs;
import org.gudy.azureus2.core3.logging.Logger;
import org.gudy.azureus2.core3.peer.PEPeer;
import org.gudy.azureus2.core3.peer.PEPeerManager;
import org.gudy.azureus2.core3.peer.PEPiece;
import org.gudy.azureus2.core3.util.AEMonitor;
import org.gudy.azureus2.core3.util.SystemTime;

import com.aelitis.azureus.core.peermanager.piecepicker.PiecePicker;

public class PEPieceImpl implements PEPiece {
    private static final LogIDs LOGID = LogIDs.PIECES;

    private final DiskManagerPiece dmPiece;
    private final PEPeerManager manager;

    private final int nbBlocks; // number of blocks in this piece
    private long creationTime;

    private final String[] requested;
    private boolean fully_requested;

    private final boolean[] downloaded;
    private boolean fully_downloaded;
    private long time_last_download;

    private final String[] writers;
    private List writes;

    private String reservedBy; // using address for when they send bad/disconnect/reconnect

    // In end game mode, this limitation isn't used
    private int speed; // slower peers dont slow down fast pieces too much

    private int resumePriority;

    private Object real_time_data;

    // experimental class level lock
    protected static final AEMonitor class_mon = new AEMonitor("PEPiece:class");

    /**
     * piece for tracking partially downloaded pieces
     * 
     * @param _manager
     *            the PEPeerManager
     * @param _dm_piece
     *            the backing dmPiece
     * @param _pieceSpeed
     *            the speed threshold for potential new requesters
     */
    public PEPieceImpl(PEPeerManager _manager, DiskManagerPiece _dm_piece, int _pieceSpeed) {
        creationTime = SystemTime.getCurrentTime();
        manager = _manager;
        dmPiece = _dm_piece;
        speed = _pieceSpeed;

        nbBlocks = dmPiece.getNbBlocks();

        requested = new String[nbBlocks];

        final boolean[] written = dmPiece.getWritten();
        if (written == null)
            downloaded = new boolean[nbBlocks];
        else
            downloaded = (boolean[]) written.clone();

        writers = new String[nbBlocks];
        writes = new ArrayList(0);
    }

    public DiskManagerPiece getDMPiece() {
        return dmPiece;
    }

    public long getCreationTime() {
        final long now = SystemTime.getCurrentTime();
        if (now >= creationTime && creationTime > 0) {
            return creationTime;
        }
        creationTime = now;
        return now;
    }

    public long getTimeSinceLastActivity() {
        final long now = SystemTime.getCurrentTime();
        final long lastWriteTime = getLastDownloadTime(now);
        if (lastWriteTime > 0) {
            return now - lastWriteTime;
        }
        final long lastCreateTime = creationTime;
        if (lastCreateTime > 0 && now >= lastCreateTime) {
            return now - lastCreateTime;
        }
        creationTime = now;
        return 0;
    }

    public long getLastDownloadTime(final long now) {
        if (time_last_download <= now)
            return time_last_download;
        return time_last_download = now;
    }

    /**
     * Tells if a block has been requested
     * 
     * @param blockNumber
     *            the block in question
     * @return true if the block is Requested already
     */
    public boolean isRequested(int blockNumber) {
        return requested[blockNumber] != null;
    }

    /**
     * Tells if a block has been downloaded
     * 
     * @param blockNumber
     *            the block in question
     * @return true if the block is downloaded already
     */
    public boolean isDownloaded(int blockNumber) {
        return downloaded[blockNumber];
    }

    /**
     * This flags the block at the given offset as having been downloaded If all blocks are now downloaed, sets the dmPiece as downloaded
     * 
     * @param blockNumber
     */
    public void setDownloaded(int offset) {
        time_last_download = SystemTime.getCurrentTime();
        downloaded[offset / DiskManager.BLOCK_SIZE] = true;
        for (int i = 0; i < nbBlocks; i++) {
            if (!downloaded[i])
                return;
        }

        fully_downloaded = true;
        fully_requested = false;
    }

    /**
     * This flags the block at the given offset as NOT having been downloaded and the whole piece as not having been fully downloaded
     * 
     * @param blockNumber
     */
    public void clearDownloaded(int offset) {
        downloaded[offset / DiskManager.BLOCK_SIZE] = false;

        fully_downloaded = false;
    }

    public boolean isDownloaded() {
        return (fully_downloaded);
    }

    public boolean[] getDownloaded() {
        return (downloaded);
    }

    public boolean hasUndownloadedBlock() {
        for (int i = 0; i < nbBlocks; i++) {

            if (!downloaded[i]) {

                return (true);
            }
        }

        return (false);
    }

    /**
     * This marks a given block as having been written by the given peer
     * 
     * @param peer
     *            the PEPeer that sent the data
     * @param blockNumber
     *            the block we're operating on
     */
    public void setWritten(PEPeer peer, int blockNumber) {
        writers[blockNumber] = peer.getIp();
        dmPiece.setWritten(blockNumber);
    }

    /**
     * This method clears the requested information for the given block unless the block has already been downloaded, in which case the writer's IP is
     * recorded as a request for the block.
     */
    public void clearRequested(int blockNumber) {
        requested[blockNumber] = downloaded[blockNumber] ? writers[blockNumber] : null;

        fully_requested = false;
    }

    public boolean isRequested() {
        return (fully_requested);
    }

    public void setRequested() {
        fully_requested = true;
    }

    /**
     * This will scan each block looking for requested blocks. For each one, it'll verify if the PEPeer for it still exists and is still willing and
     * able to upload data. If not, it'll unmark the block as requested.
     * 
     * @return int of how many were cleared (0 to nbBlocks)
     */
    /*
     * public int checkRequests() { if (getTimeSinceLastActivity() <30 *1000) return 0; int cleared =0; boolean nullPeer =false; for (int i =0; i
     * <nbBlocks; i++) { if (!downloaded[i] &&!dmPiece.isWritten(i)) { final String requester =requested[i]; final PEPeerTransport pt; if (requester
     * !=null) { pt =manager.getTransportFromAddress(requester); if (pt !=null) { pt.setSnubbed(true); if (!pt.isDownloadPossible()) {
     * clearRequested(i); cleared++; } } else { nullPeer =true; clearRequested(i); cleared++; } } } } if (cleared >0) { dmPiece.clearRequested(); if
     * (Logger.isEnabled()) Logger.log(new LogEvent(dmPiece.getManager().getTorrent(), LOGID, LogEvent.LT_WARNING, "checkRequests(): piece #"
     * +getPieceNumber()+" cleared " +cleared +" requests." + (nullPeer ?" Null peer was detected." :""))); } return cleared; }
     */

    /*
     * Parg: replaced above commented out checking with one that verifies that the requests still exist. As piece-picker activity and peer disconnect
     * logic is multi-threaded and full of holes, this is a stop-gap measure to prevent a piece from being left with requests that no longer exist
     */

    public void checkRequests() {
        if (getTimeSinceLastActivity() < 30 * 1000) {

            return;
        }

        int cleared = 0;

        for (int i = 0; i < nbBlocks; i++) {

            if (!downloaded[i] && !dmPiece.isWritten(i)) {

                final String requester = requested[i];

                if (requester != null) {

                    if (!manager.requestExists(requester, getPieceNumber(), i * DiskManager.BLOCK_SIZE, getBlockSize(i))) {

                        clearRequested(i);

                        cleared++;
                    }
                }
            }
        }

        if (cleared > 0) {

            if (Logger.isEnabled())
                Logger.log(new LogEvent(dmPiece.getManager().getTorrent(), LOGID, LogEvent.LT_WARNING, "checkRequests(): piece #"
                        + getPieceNumber() + " cleared " + cleared + " requests"));
        } else {

            if (fully_requested && getNbUnrequested() > 0) {

                if (Logger.isEnabled())
                    Logger.log(new LogEvent(dmPiece.getManager().getTorrent(), LOGID, LogEvent.LT_WARNING, "checkRequests(): piece #"
                            + getPieceNumber() + " reset fully requested"));

                fully_requested = false;
            }
        }
    }

    /**
     * @return true if the piece has any blocks that are not; Downloaded, Requested, or Written
     */
    public boolean hasUnrequestedBlock() {
        final boolean[] written = dmPiece.getWritten();
        for (int i = 0; i < nbBlocks; i++) {
            if (!downloaded[i] && requested[i] == null && (written == null || !written[i]))
                return true;
        }
        return false;
    }

    /**
     * This method scans a piece for the first unrequested block. Upon finding it, it counts how many are unrequested up to nbWanted. The blocks are
     * marked as requested by the PEPeer Assumption - single threaded access to this TODO: this should return the largest span equal or smaller than
     * nbWanted OR, probably a different method should do that, so this one can support 'more sequential' picking
     */
    public int[] getAndMarkBlocks(PEPeer peer, int nbWanted, int[] request_hint, boolean reverse_order) {
        final String ip = peer.getIp();

        final boolean[] written = dmPiece.getWritten();

        if (request_hint != null) {

            // try to honour the hint first

            int hint_block_start = request_hint[1] / DiskManager.BLOCK_SIZE;
            int hint_block_end = (request_hint[1] + request_hint[2] - 1) / DiskManager.BLOCK_SIZE;

            if (reverse_order) {

                for (int i = Math.min(nbBlocks - 1, hint_block_end); i >= hint_block_start; i--) {

                    int blocksFound = 0;
                    int block_index = i;

                    while (blocksFound < nbWanted && block_index < nbBlocks && !downloaded[block_index] && requested[block_index] == null
                            && (written == null || !written[block_index])) {

                        requested[block_index] = ip;
                        blocksFound++;
                        block_index--;
                    }
                    if (blocksFound > 0) {
                        return new int[] { block_index + 1, blocksFound };
                    }
                }
            } else {
                for (int i = hint_block_start; i < nbBlocks && i <= hint_block_end; i++) {

                    int blocksFound = 0;
                    int block_index = i;

                    while (blocksFound < nbWanted && block_index < nbBlocks && !downloaded[block_index] && requested[block_index] == null
                            && (written == null || !written[block_index])) {

                        requested[block_index] = ip;
                        blocksFound++;
                        block_index++;
                    }
                    if (blocksFound > 0) {
                        return new int[] { i, blocksFound };
                    }
                }
            }
        }

        // scan piece to find first free block

        if (reverse_order) {

            for (int i = nbBlocks - 1; i >= 0; i--) {

                int blocksFound = 0;
                int block_index = i;

                while (blocksFound < nbWanted && block_index >= 0 && !downloaded[block_index] && requested[block_index] == null
                        && (written == null || !written[block_index])) {

                    requested[block_index] = ip;
                    blocksFound++;
                    block_index--;
                }
                if (blocksFound > 0) {
                    return new int[] { block_index + 1, blocksFound };
                }
            }
        } else {

            for (int i = 0; i < nbBlocks; i++) {

                int blocksFound = 0;
                int block_index = i;

                while (blocksFound < nbWanted && block_index < nbBlocks && !downloaded[block_index] && requested[block_index] == null
                        && (written == null || !written[block_index])) {

                    requested[block_index] = ip;
                    blocksFound++;
                    block_index++;
                }
                if (blocksFound > 0) {
                    return new int[] { i, blocksFound };
                }
            }
        }
        return new int[] { -1, 0 };
    }

    public void getAndMarkBlock(PEPeer peer, int index) {
        requested[index] = peer.getIp();

        if (getNbUnrequested() <= 0) {

            setRequested();
        }
    }

    public int getNbRequests() {
        int result = 0;
        for (int i = 0; i < nbBlocks; i++) {
            if (!downloaded[i] && requested[i] != null)
                result++;
        }
        return result;
    }

    public int getNbUnrequested() {
        int result = 0;
        final boolean[] written = dmPiece.getWritten();
        for (int i = 0; i < nbBlocks; i++) {
            if (!downloaded[i] && requested[i] == null && (written == null || !written[i]))
                result++;
        }
        return result;
    }

    /**
     * Assumption - single threaded with getAndMarkBlock
     */
    public boolean setRequested(PEPeer peer, int blockNumber) {
        if (!downloaded[blockNumber]) {
            requested[blockNumber] = peer.getIp();
            return true;
        }
        return false;
    }

    public boolean isRequestable() {
        return (dmPiece.isDownloadable() && !(fully_downloaded || fully_requested));
    }

    public int getBlockSize(int blockNumber) {
        if (blockNumber == (nbBlocks - 1)) {

            int length = dmPiece.getLength();

            if ((length % DiskManager.BLOCK_SIZE) != 0) {

                return (length % DiskManager.BLOCK_SIZE);
            }
        }

        return DiskManager.BLOCK_SIZE;
    }

    public int getBlockNumber(int offset) {
        return offset / DiskManager.BLOCK_SIZE;
    }

    public int getNbBlocks() {
        return nbBlocks;
    }

    public List getPieceWrites() {
        List result;
        try {
            class_mon.enter();

            result = new ArrayList(writes);
        } finally {

            class_mon.exit();
        }
        return result;
    }

    public List getPieceWrites(int blockNumber) {
        final List result;
        try {
            class_mon.enter();

            result = new ArrayList(writes);

        } finally {

            class_mon.exit();
        }
        final Iterator iter = result.iterator();
        while (iter.hasNext()) {
            final PEPieceWriteImpl write = (PEPieceWriteImpl) iter.next();
            if (write.getBlockNumber() != blockNumber)
                iter.remove();
        }
        return result;
    }

    public List getPieceWrites(PEPeer peer) {
        final List result;
        try {
            class_mon.enter();

            result = new ArrayList(writes);
        } finally {
            class_mon.exit();
        }
        final Iterator iter = result.iterator();
        while (iter.hasNext()) {
            PEPieceWriteImpl write = (PEPieceWriteImpl) iter.next();
            if (peer == null || !peer.getIp().equals(write.getSender()))
                iter.remove();
        }
        return result;
    }

    public List getPieceWrites(String ip) {
        final List result;

        try {
            class_mon.enter();

            result = new ArrayList(writes);

        } finally {

            class_mon.exit();
        }

        final Iterator iter = result.iterator();

        while (iter.hasNext()) {

            final PEPieceWriteImpl write = (PEPieceWriteImpl) iter.next();

            if (!write.getSender().equals(ip)) {

                iter.remove();
            }
        }

        return result;
    }

    public void reset() {
        dmPiece.reset();
        for (int i = 0; i < nbBlocks; i++) {
            requested[i] = null;
            downloaded[i] = false;
            writers[i] = null;
        }
        fully_downloaded = false;
        time_last_download = 0;
        reservedBy = null;
        real_time_data = null;
    }

    public Object getRealTimeData() {
        return (real_time_data);
    }

    public void setRealTimeData(Object o) {
        real_time_data = o;
    }

    protected void addWrite(PEPieceWriteImpl write) {
        try {
            class_mon.enter();

            writes.add(write);

        } finally {

            class_mon.exit();
        }
    }

    public void addWrite(int blockNumber, String sender, byte[] hash, boolean correct) {
        addWrite(new PEPieceWriteImpl(blockNumber, sender, hash, correct));
    }

    public String[] getWriters() {
        return writers;
    }

    public int getSpeed() {
        return speed;
    }

    public void setSpeed(int newSpeed) {
        speed = newSpeed;
    }

    public void setLastRequestedPeerSpeed(int peerSpeed) {
        // Up the speed on this piece?
        if (peerSpeed > speed) {
            speed++;
        }
    }

    /**
     * @return Returns the manager.
     */
    public PEPeerManager getManager() {
        return manager;
    }

    public void setReservedBy(String peer) {
        reservedBy = peer;
    }

    public String getReservedBy() {
        return reservedBy;
    }

    /**
     * for a block that's already downloadedt, mark up the piece so that the block will get downloaded again. This is used when the piece fails
     * hash-checking.
     */
    public void reDownloadBlock(int blockNumber) {
        downloaded[blockNumber] = false;
        requested[blockNumber] = null;
        fully_downloaded = false;
        writers[blockNumber] = null;
        dmPiece.reDownloadBlock(blockNumber);
    }

    /**
     * finds all blocks downloaded by the given address and marks them up for re-downloading
     * 
     * @param address
     *            String
     */
    public void reDownloadBlocks(String address) {
        for (int i = 0; i < writers.length; i++) {
            final String writer = writers[i];

            if (writer != null && writer.equals(address))
                reDownloadBlock(i);
        }
    }

    public void setResumePriority(int p) {
        resumePriority = p;
    }

    public int getResumePriority() {
        return resumePriority;
    }

    /**
     * @return int of availability in the swarm for this piece
     * @see org.gudy.azureus2.core3.peer.PEPeerManager.getAvailability(int pieceNumber)
     */
    public int getAvailability() {
        return manager.getAvailability(dmPiece.getPieceNumber());
    }

    /**
     * This support method returns how many blocks have already been written from the dmPiece
     * 
     * @return int from dmPiece.getNbWritten()
     * @see org.gudy.azureus2.core3.disk.DiskManagerPiece.getNbWritten()
     */
    public int getNbWritten() {
        return dmPiece.getNbWritten();
    }

    /**
     * This support method returns the dmPiece's written array
     * 
     * @return boolean[] from the dmPiece
     * @see org.gudy.azureus2.core3.disk.DiskManagerPiece.getWritten()
     */
    public boolean[] getWritten() {
        return dmPiece.getWritten();
    }

    public boolean isWritten() {
        return dmPiece.isWritten();
    }

    public boolean isWritten(int block) {
        return dmPiece.isWritten(block);
    }

    public int getPieceNumber() {
        return dmPiece.getPieceNumber();
    }

    public int getLength() {
        return dmPiece.getLength();
    }

    public void setRequestable() {
        fully_downloaded = false;
        fully_requested = false;

        dmPiece.setDownloadable();
    }

    public String getString() {
        String text = "";

        PiecePicker pp = manager.getPiecePicker();

        text += (isRequestable() ? "reqable," : "");
        text += "req=" + getNbRequests() + ",";
        text += (isRequested() ? "reqstd," : "");
        text += (isDownloaded() ? "downed," : "");
        text += (getReservedBy() != null ? "resrv," : "");
        text += "speed=" + getSpeed() + ",";
        text += (pp == null ? ("pri=" + getResumePriority()) : pp.getPieceString(dmPiece.getPieceNumber()));

        if (text.endsWith(",")) {
            text = text.substring(0, text.length() - 1);
        }

        return (text);
    }
}